/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.geogebra.common.move.ggtapi.models.json;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.geogebra.common.util.StringUtil;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

// Note: this class was written without inspecting the non-free org.json sourcecode.
/**
 * Implements {@link JSONObject#toString} and {@link JSONArray#toString}. Most
 * application developers should use those methods directly and disregard this
 * API. For example:
 * 
 * <pre>
 * JSONObject object = ...
 * String json = object.toString();
 * </pre>
 *
 * <p>
 * Stringers only encode well-formed JSON strings. In particular:
 * <ul>
 * <li>The stringer must have exactly one top-level array or object.
 * <li>Lexical scopes must be balanced: every call to {@link #array} must have a
 * matching call to {@link #endArray} and every call to {@link #object} must
 * have a matching call to {@link #endObject}.
 * <li>Arrays may not contain keys (property names).
 * <li>Objects must alternate keys (property names) and values.
 * <li>Values are inserted with either literal {@link #value(Object) value}
 * calls, or by nesting arrays or objects.
 * </ul>
 * Calls that would result in a malformed JSON string will fail with a
 * {@link JSONException}.
 *
 * <p>
 * This class provides no facility for pretty-printing (ie. indenting) output.
 * To encode indented output, use {@link JSONObject#toString(int)} or
 * {@link JSONArray#toString(int)}.
 *
 * <p>
 * Some implementations of the API support at most 20 levels of nesting.
 * Attempts to create more than 20 levels of nesting may fail with a
 * {@link JSONException}.
 *
 * <p>
 * Each stringer may be used to encode a single top level value. Instances of
 * this class are not thread safe. Although this class is nonfinal, it was not
 * designed for inheritance and should not be subclassed. In particular,
 * self-use by overrideable methods is not specified. See <i>Effective Java</i>
 * Item 17, "Design and Document or inheritance or else prohibit it" for further
 * information.
 */
public class JSONStringer {
	/** The output data, containing at most one top-level array or object. */
	final StringBuilder out = new StringBuilder();

	/**
	 * Lexical scoping elements within this stringer, necessary to insert the
	 * appropriate separator characters (ie. commas and colons) and to detect
	 * nesting errors.
	 */
	enum Scope {
		/**
		 * An array with no elements requires no separators or newlines before
		 * it is closed.
		 */
		EMPTY_ARRAY,
		/**
		 * A array with at least one value requires a comma and newline before
		 * the next element.
		 */
		NONEMPTY_ARRAY,
		/**
		 * An object with no keys or values requires no separators or newlines
		 * before it is closed.
		 */
		EMPTY_OBJECT,
		/**
		 * An object whose most recent element is a key. The next element must
		 * be a value.
		 */
		DANGLING_KEY,
		/**
		 * An object with at least one name/value pair requires a comma and
		 * newline before the next element.
		 */
		NONEMPTY_OBJECT,
		/**
		 * A special bracketless array needed by JSONStringer.join() and
		 * JSONObject.quote() only. Not used for JSON encoding.
		 */
		NULL,
	}

	/**
	 * Unlike the original implementation, this stack isn't limited to 20 levels
	 * of nesting.
	 */
	private final List<Scope> stack = new ArrayList<>();
	/**
	 * A string containing a full set of spaces for a single level of
	 * indentation, or null for no pretty printing.
	 */
	private final String indent;

	public JSONStringer() {
		indent = null;
	}

	JSONStringer(int indentSpaces) {
		char[] indentChars = new char[indentSpaces];
		Arrays.fill(indentChars, ' ');
		indent = new String(indentChars);
	}

	/**
	 * Begins encoding a new array. Each call to this method must be paired with
	 * a call to {@link #endArray}.
	 *
	 * @return this stringer.
	 */
	public JSONStringer array() throws JSONException {
		return open(Scope.EMPTY_ARRAY, "[");
	}

	/**
	 * Ends encoding the current array.
	 *
	 * @return this stringer.
	 */
	public JSONStringer endArray() throws JSONException {
		return close(Scope.EMPTY_ARRAY, Scope.NONEMPTY_ARRAY, "]");
	}

	/**
	 * Begins encoding a new object. Each call to this method must be paired
	 * with a call to {@link #endObject}.
	 *
	 * @return this stringer.
	 */
	public JSONStringer object() throws JSONException {
		return open(Scope.EMPTY_OBJECT, "{");
	}

	/**
	 * Ends encoding the current object.
	 *
	 * @return this stringer.
	 */
	public JSONStringer endObject() throws JSONException {
		return close(Scope.EMPTY_OBJECT, Scope.NONEMPTY_OBJECT, "}");
	}

	/**
	 * Enters a new scope by appending any necessary whitespace and the given
	 * bracket.
	 */
	JSONStringer open(Scope empty, String openBracket) throws JSONException {
		if (stack.isEmpty() && out.length() > 0) {
			throw new JSONException(
					"Nesting problem: multiple top-level roots");
		}
		beforeValue();
		stack.add(empty);
		out.append(openBracket);
		return this;
	}

	/**
	 * Closes the current scope by appending any necessary whitespace and the
	 * given bracket.
	 */
	JSONStringer close(Scope empty, Scope nonempty, String closeBracket)
			throws JSONException {
		Scope context = peek();
		if (context != nonempty && context != empty) {
			throw new JSONException("Nesting problem");
		}
		stack.remove(stack.size() - 1);
		if (context == nonempty) {
			newline();
		}
		out.append(closeBracket);
		return this;
	}

	/**
	 * Returns the value on the top of the stack.
	 */
	private Scope peek() throws JSONException {
		if (stack.isEmpty()) {
			throw new JSONException("Nesting problem");
		}
		return stack.get(stack.size() - 1);
	}

	/**
	 * Replace the value on the top of the stack with the given value.
	 */
	private void replaceTop(Scope topOfStack) {
		stack.set(stack.size() - 1, topOfStack);
	}

	/**
	 * Encodes {@code value}.
	 *
	 * @param value
	 *            a {@link JSONObject}, {@link JSONArray}, String, Boolean,
	 *            Integer, Long, Double or null. May not be
	 *            {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
	 *            infinities}.
	 * @return this stringer.
	 */
	public JSONStringer value(Object value) throws JSONException {
		if (stack.isEmpty()) {
			throw new JSONException("Nesting problem");
		}
		if (value instanceof JSONArray) {
			((JSONArray) value).writeTo(this);
			return this;
		} else if (value instanceof JSONObject) {
			((JSONObject) value).writeTo(this);
			return this;
		}
		beforeValue();
		if (value == null || value instanceof Boolean
				|| value == JSONObject.NULL) {
			out.append(value);
		} else if (value instanceof Number) {
			out.append(JSONObject.numberToString((Number) value));
		} else {
			string(value.toString());
		}
		return this;
	}

	/**
	 * Encodes {@code value} to this stringer.
	 *
	 * @return this stringer.
	 */
	public JSONStringer value(boolean value) throws JSONException {
		if (stack.isEmpty()) {
			throw new JSONException("Nesting problem");
		}
		beforeValue();
		out.append(value);
		return this;
	}

	/**
	 * Encodes {@code value} to this stringer.
	 *
	 * @param value
	 *            a finite value. May not be {@link Double#isNaN() NaNs} or
	 *            {@link Double#isInfinite() infinities}.
	 * @return this stringer.
	 */
	public JSONStringer value(double value) throws JSONException {
		if (stack.isEmpty()) {
			throw new JSONException("Nesting problem");
		}
		beforeValue();
		out.append(JSONObject.numberToString(value));
		return this;
	}

	/**
	 * Encodes {@code value} to this stringer.
	 *
	 * @return this stringer.
	 */
	public JSONStringer value(long value) throws JSONException {
		if (stack.isEmpty()) {
			throw new JSONException("Nesting problem");
		}
		beforeValue();
		out.append(value);
		return this;
	}

	private void string(String value) {
		out.append("\"");
		for (int i = 0, length = value.length(); i < length; i++) {
			char c = value.charAt(i);
			/*
			 * From RFC 4627, "All Unicode characters may be placed within the
			 * quotation marks except for the characters that must be escaped:
			 * quotation mark, reverse solidus, and the control characters
			 * (U+0000 through U+001F)."
			 */
			switch (c) {
			case '"':
			case '\\':
			case '/':
				out.append('\\').append(c);
				break;
			case '\t':
				out.append("\\t");
				break;
			case '\b':
				out.append("\\b");
				break;
			case '\n':
				out.append("\\n");
				break;
			case '\r':
				out.append("\\r");
				break;
			case '\f':
				out.append("\\f");
				break;
			default:
				if (c <= 0x1F) {
					out.append(StringUtil.toJavaString(c + ""));
					// out.append(String.format("\\u%04x", (int) c));
				} else {
					out.append(c);
				}
				break;
			}
		}
		out.append("\"");
	}

	private void newline() {
		if (indent == null) {
			return;
		}
		out.append("\n");
		for (int i = 0; i < stack.size(); i++) {
			out.append(indent);
		}
	}

	/**
	 * Encodes the key (property name) to this stringer.
	 *
	 * @param name
	 *            the name of the forthcoming value. May not be null.
	 * @return this stringer.
	 */
	public JSONStringer key(String name) throws JSONException {
		if (name == null) {
			throw new JSONException("Names must be non-null");
		}
		beforeKey();
		string(name);
		return this;
	}

	/**
	 * Inserts any necessary separators and whitespace before a name. Also
	 * adjusts the stack to expect the key's value.
	 */
	private void beforeKey() throws JSONException {
		Scope context = peek();
		if (context == Scope.NONEMPTY_OBJECT) { // first in object
			out.append(',');
		} else if (context != Scope.EMPTY_OBJECT) { // not in an object!
			throw new JSONException("Nesting problem");
		}
		newline();
		replaceTop(Scope.DANGLING_KEY);
	}

	/**
	 * Inserts any necessary separators and whitespace before a literal value,
	 * inline array, or inline object. Also adjusts the stack to expect either a
	 * closing bracket or another element.
	 */
	private void beforeValue() throws JSONException {
		if (stack.isEmpty()) {
			return;
		}
		Scope context = peek();
		if (context == Scope.EMPTY_ARRAY) { // first in array
			replaceTop(Scope.NONEMPTY_ARRAY);
			newline();
		} else if (context == Scope.NONEMPTY_ARRAY) { // another in array
			out.append(',');
			newline();
		} else if (context == Scope.DANGLING_KEY) { // value for key
			out.append(indent == null ? ":" : ": ");
			replaceTop(Scope.NONEMPTY_OBJECT);
		} else if (context != Scope.NULL) {
			throw new JSONException("Nesting problem");
		}
	}

	/**
	 * Returns the encoded JSON string.
	 *
	 * <p>
	 * If invoked with unterminated arrays or unclosed objects, this method's
	 * return value is undefined.
	 *
	 * <p>
	 * <strong>Warning:</strong> although it contradicts the general contract of
	 * {@link Object#toString}, this method returns null if the stringer
	 * contains no data.
	 */
	@Override
	@SuppressFBWarnings({ "NP_TOSTRING_COULD_RETURN_NULL",
			"API specifies this" })
	public String toString() {
		return out.length() == 0 ? null : out.toString();
	}
}